////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2020 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 3 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
////////////////////////////////////////////////////////////////////////////////

package org.sonar.plugins.checkstyle;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.annotation.Nullable;

import org.apache.commons.lang.StringEscapeUtils;
import org.apache.commons.lang.StringUtils;
import org.sonar.api.ExtensionPoint;
import org.sonar.api.batch.ScannerSide;
import org.sonar.api.batch.rule.ActiveRules;
import org.sonar.api.config.Configuration;
import org.sonar.api.profiles.ProfileExporter;
import org.sonar.api.profiles.RulesProfile;
import org.sonar.api.rules.ActiveRule;
import org.sonar.plugins.checkstyle.rule.ActiveRuleWrapper;
import org.sonar.plugins.checkstyle.rule.ActiveRuleWrapperScannerImpl;
import org.sonar.plugins.checkstyle.rule.ActiveRuleWrapperServerImpl;

import com.google.common.annotations.VisibleForTesting;

@ExtensionPoint
@ScannerSide
public class CheckstyleProfileExporter extends ProfileExporter {

    public static final String DOCTYPE_DECLARATION =
        "<!DOCTYPE module PUBLIC \"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN\" "
        + "\"https://checkstyle.org/dtds/configuration_1_3.dtd\">";
    private static final String CLOSE_MODULE = "</module>";

    private final Configuration configuration;

    public CheckstyleProfileExporter(Configuration configuration) {
        super(CheckstyleConstants.REPOSITORY_KEY, CheckstyleConstants.PLUGIN_NAME);
        this.configuration = configuration;
        setSupportedLanguages(CheckstyleConstants.JAVA_KEY);
        setMimeType("application/xml");
    }

    @Override
    public void exportProfile(RulesProfile profile, Writer writer) {
        try {
            final List<ActiveRule> activeRules = profile
                    .getActiveRulesByRepository(CheckstyleConstants.REPOSITORY_KEY);
            if (activeRules != null) {
                final List<ActiveRuleWrapper> activeRuleWrappers = new ArrayList<>();
                for (ActiveRule activeRule : activeRules) {
                    activeRuleWrappers.add(new ActiveRuleWrapperServerImpl(activeRule));
                }
                final Map<String, List<ActiveRuleWrapper>> activeRulesByConfigKey =
                        arrangeByConfigKey(activeRuleWrappers);
                generateXml(writer, activeRulesByConfigKey);
            }
        }
        catch (IOException ex) {
            throw new IllegalStateException("Fail to export the profile " + profile, ex);
        }
    }

    public void exportProfile(ActiveRules activeRules, Writer writer) {
        try {
            final List<ActiveRuleWrapper> activeRuleWrappers = new ArrayList<>();
            for (org.sonar.api.batch.rule.ActiveRule activeRule : activeRules
                    .findByRepository(CheckstyleConstants.REPOSITORY_KEY)) {
                activeRuleWrappers.add(new ActiveRuleWrapperScannerImpl(activeRule));
            }
            final Map<String, List<ActiveRuleWrapper>> activeRulesByConfigKey =
                    arrangeByConfigKey(activeRuleWrappers);
            generateXml(writer, activeRulesByConfigKey);
        }
        catch (IOException ex) {
            throw new IllegalStateException("Fail to export active rules.", ex);
        }

    }

    private void generateXml(Writer writer, Map<String,
            List<ActiveRuleWrapper>> activeRulesByConfigKey) throws IOException {
        appendXmlHeader(writer);
        appendTabWidth(writer);
        appendCustomFilters(writer);
        appendCheckerModules(writer, activeRulesByConfigKey);
        appendTreeWalker(writer, activeRulesByConfigKey);
        appendXmlFooter(writer);
    }

    private static void appendXmlHeader(Writer writer) throws IOException {
        writer.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + DOCTYPE_DECLARATION
                + "<!-- Generated by Sonar -->" + "<module name=\"Checker\">");
    }

    private void appendCustomFilters(Writer writer) throws IOException {
        final String filtersXml = configuration.get(CheckstyleConstants.CHECKER_FILTERS_KEY)
                .orElse(null);
        writer.append(filtersXml);
    }

    private void appendTabWidth(Writer writer) throws IOException {
        final String tabWidth = configuration.get(CheckstyleConstants.CHECKER_TAB_WIDTH)
                .orElse(null);
        appendModuleProperty(writer, "tabWidth", tabWidth);
    }

    private static void appendCheckerModules(Writer writer,
            Map<String, List<ActiveRuleWrapper>> activeRulesByConfigKey) throws IOException {
        for (Map.Entry<String, List<ActiveRuleWrapper>> entry : activeRulesByConfigKey.entrySet()) {
            final String configKey = entry.getKey();
            if (!isInTreeWalker(configKey)) {
                final List<ActiveRuleWrapper> activeRules = entry.getValue();
                for (ActiveRuleWrapper activeRule : activeRules) {
                    appendModule(writer, activeRule);
                }
            }
        }
    }

    private void appendTreeWalker(Writer writer,
            Map<String, List<ActiveRuleWrapper>> activeRulesByConfigKey) throws IOException {
        writer.append("<module name=\"TreeWalker\">");
        if (isSuppressWarningsEnabled()) {
            writer.append("<module name=\"SuppressWarningsHolder\"/> ");
        }
        final List<String> ruleSet = new ArrayList<>(activeRulesByConfigKey.keySet());
        Collections.sort(ruleSet);
        for (String configKey : ruleSet) {
            if (isInTreeWalker(configKey)) {
                final List<ActiveRuleWrapper> activeRules = activeRulesByConfigKey.get(configKey);
                for (ActiveRuleWrapper activeRule : activeRules) {
                    appendModule(writer, activeRule);
                }
            }
        }
        // append Treewalker filters
        final String filtersXml = configuration
                .get(CheckstyleConstants.TREEWALKER_FILTERS_KEY)
                .orElse(null);
        writer.append(filtersXml);

        writer.append(CLOSE_MODULE);
    }

    private boolean isSuppressWarningsEnabled() {
        final String filtersXml = configuration.get(CheckstyleConstants.CHECKER_FILTERS_KEY)
                .orElse(null);
        boolean result = false;
        if (filtersXml != null) {
            result = filtersXml.contains("<module name=\"SuppressWarningsFilter\" />");
        }
        return result;
    }

    private static void appendXmlFooter(Writer writer) throws IOException {
        writer.append(CLOSE_MODULE);
    }

    @VisibleForTesting
    static boolean isInTreeWalker(String configKey) {
        return StringUtils.startsWithIgnoreCase(configKey, "Checker/TreeWalker/");
    }

    private static Map<String, List<ActiveRuleWrapper>> arrangeByConfigKey(
            Collection<ActiveRuleWrapper> activeRules) {
        final Map<String, List<ActiveRuleWrapper>> result = new HashMap<>();
        for (ActiveRuleWrapper activeRule : activeRules) {
            final String key = activeRule.getInternalKey();
            if (result.containsKey(key)) {
                final List<ActiveRuleWrapper> rules = result.get(key);
                rules.add(activeRule);
            }
            else {
                final List<ActiveRuleWrapper> rules = new ArrayList<>();
                rules.add(activeRule);
                result.put(key, rules);
            }
        }
        return result;
    }

    private static void appendModule(Writer writer, ActiveRuleWrapper activeRule)
            throws IOException {
        final String moduleName = StringUtils.substringAfterLast(activeRule.getInternalKey(), "/");
        writer.append("<module name=\"");
        StringEscapeUtils.escapeXml(writer, moduleName);
        writer.append("\">");
        if (activeRule.getTemplateRuleKey() != null) {
            appendModuleProperty(writer, "id", activeRule.getRuleKey());
        }
        appendModuleProperty(writer, "severity", activeRule.getSeverity());
        appendRuleParameters(writer, activeRule);
        writer.append(CLOSE_MODULE);
    }

    private static void appendRuleParameters(Writer writer, ActiveRuleWrapper activeRule)
            throws IOException {
        for (Map.Entry<String, String> param : activeRule.getParams().entrySet()) {
            if (StringUtils.isNotBlank(param.getValue())) {
                appendModuleProperty(writer, param.getKey(), param.getValue());
            }
        }
    }

    private static void appendModuleProperty(Writer writer, String propertyKey,
            @Nullable String propertyValue) throws IOException {
        if (StringUtils.isNotBlank(propertyValue)) {
            writer.append("<property name=\"");
            StringEscapeUtils.escapeXml(writer, propertyKey);
            writer.append("\" value=\"");
            StringEscapeUtils.escapeXml(writer, propertyValue);
            writer.append("\"/>");
        }
    }

}
